Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.

...powered by www.netzwerkartist.de...

 << zurück
Visual C# 2005 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual C# 2005

Visual C# 2005
1.320 S., mit 2 CDs, 59,90 Euro
Galileo Computing
ISBN 3-89842-586-X
gp Kapitel 11 Multithreading und asynchrone Methodenaufrufe
  gp 11.1 Prozesse und Threads
    gp 11.1.1 Threadzustände und Prioritäten
    gp 11.1.2 Einsatz von mehreren Threads
  gp 11.2 Die Entwicklung einer Multithread-Anwendung
    gp 11.2.1 Die Klasse »Thread«
    gp 11.2.2 Threadpools nutzen
  gp 11.3 Die Synchronisation von Threads
    gp 11.3.1 Unsynchronisierte Threads
    gp 11.3.2 Der »Monitor« zur Synchronisation
    gp 11.3.3 Das Synchronisationsobjekt »Mutex«
    gp 11.3.4 Das Attribut »MethodImpl«
  gp 11.4 Asynchrone Methodenaufrufe
    gp 11.4.1 Asynchroner Methodenaufruf
    gp 11.4.2 Asynchroner Aufruf mit Rückgabewerten
    gp 11.4.3 Eine Klasse mit asynchronen Methodenaufrufen


Galileo Computing

11.4 Asynchrone Methodenaufrufe  downtop

Wird aus einer Methode A heraus die Methode B aufgerufen, wird A erst dann mit den Operationen fortfahren, wenn B vollständig abgearbeitet ist. Die Ausführung der beiden Methoden erfolgt hintereinander, was als synchron bezeichnet wird. Synchrone Operationen haben einen gravierenden Nachteil, denn solange die Methode B ausgeführt wird, ist die Methode A blockiert. Um diese Problematik zu vermeiden, sollten beide Methoden asynchron, d. h., parallel nebeneinander operieren.

Asynchrone Bearbeitung setzt mindestens zwei Threads voraus. Sie haben auf den vergangenen Seiten die wichtigsten Techniken kennen gelernt, um mit Threads zu arbeiten. Sie wissen nun, wie Sie Threads erzeugen und diese möglicherweise synchronisieren können, damit Elemente nicht in einem ungültigen Zustand hinterlassen werden. Ihnen dürfte dabei nicht entgangen sein, dass die Technik sehr komplex ist und einer genauen Planung bedarf, um keine unbeabsichtigten und bösen Überraschungen zu erleben.

Sicherlich erinnern Sie sich noch an das Beispiel der Pumpeneinschaltung in Kapitel 7.5.1, als wir uns mit den Delegaten auseinander gesetzt haben. Die höchste Entwicklungsstufe hatten wir erreicht, als es uns gelungen war, den Client mittels Rückruf vom erfolgreichen Einschaltvorgang zu benachrichtigen (Beispiel SimpleDelegate). Die Lösung funktionierte tadellos, war aber – wenn man es ganz ehrlich und selbstkritisch beurteilt – unbefriedigend, denn sie setzte voraus, dass der Einschaltvorgang nur eine kurze Zeitspanne dauert. Das ist bei mechanischen Objekten sicherlich nicht gewährleistet. Um der Realität besser zu entsprechen, muss die Klassendefinition der Pumpe noch einmal überarbeitet werden,

Auch in der .NET-Klassenbibliothek finden sich sehr viele Klassen, die Dienste anbieten, deren Ausführung möglicherweise länger dauern kann. Die Dateioperationen zum Lesen und Schreiben zählen dazu. Betrachten wir dazu exemplarisch die Klasse FileStream, die das Schreiben in eine Datei bzw. das Lesen aus einer Datei ermöglicht. (Anmerkung: Wir werden uns den Klassen zur Ein- und Ausgabe in Kaptitel 12 zuwenden.) Neben den obligatorischen Methoden Read und Write, die beide synchron ausgeführt werden, werden von dieser Klasse in weiser Voraussicht des .NET-Entwicklerteams auch die asynchron operierenden Methoden BeginRead und BeginWrite veröffentlicht. Sehen wir uns kurz die Definition der erstgenannten Methode an, die aus einem Datenstrom in ein byte-Array einliest:


public override IAsyncResult BeginRead(byte[] array,
                                     int offset,
                                     int numBytes,
                                     AsyncCallback userCallback,
                                     object stateObject);

Der Rückgabewert des Methodenaufrufs ist ein Objekt, das die Schnittstelle IAsyncResult implementiert. Der Parameter vom Typ AsyncCallback ist ein Delegat, der eine Methode im Client beschreibt, die nach der Beendigung der Leseoperation aufgerufen wird.

Sowohl BeginRead als auch BeginWrite haben jeweils eine Partnermethode: EndRead und EndWrite:


public override int EndRead(IAsyncResult asyncResult);
public override void EndWrite(IAsyncResult asyncResult);

Auch diese Methoden erwarten eine Referenz vom Typ IAsyncResult.

Zwei Dinge fallen sofort auf:

1.  Die Methoden arbeiten gemäß Dokumentation asynchron, ohne dass im Aufrufer explizit ein separater Thread gestartet werden muss. Diese Leistung wird von den Methoden intern erbracht.
2. Es treten zwei Typen auf, denen Sie hier zum ersten Mal begegnen und deren Bedeutung noch unbekannt ist: IAsyncResult und AsyncCallback.
       

Wir wollen uns nun mit der Codierung einer asynchronen Ausführung beschäftigen. Danach wird auch die im ersten Moment sehr kompliziert erscheinende Parameterliste asynchron arbeitender Methoden (wie BeginRead) in einem anderen Licht erscheinen.


Galileo Computing

11.4.1 Asynchroner Methodenaufruf  downtop

Der C#-Compiler stellt mit BeginInvoke und EndInvoke einem Delegaten zwei Methoden zur Verfügung, die im Rahmen einer asynchronen Operation von entscheidender Bedeutung sind.


Beachten Sie bitte, dass es sich bei BeginInvoke und EndInvoke um sprachspezifische Methoden handelt, die nicht in der Klasse Delegate definiert sind, aber dennoch von jedem .NET-Compiler veröffentlicht werden sollten – möglicherweise auch unter einem anderen Namen.

Die Methode BeginInvoke ist sehr mächtig, denn mit ihrem Aufruf auf die Referenz eines Delegaten wird ein Hintergrundthread erzeugt, in dem die vom Delegaten beschriebene Methode ausgeführt wird. Der aufrufende Thread macht mit seiner eigenen Arbeit weiter, anstatt auf die Beendigung der aufgerufenen Methode zu warten.

Dazu ein kleines Beispiel. Nehmen wir an, dass die Methode AsyncTestProc, die eine längere Zeit zur Ausführung benötigt, aufgerufen werden soll. AsyncTestProc sei wie folgt definiert:


public void AsyncTestProc() {
  for(int i = 0; i <= 30; i++) {
    Console.Write(".X.");
    Thread.Sleep(10);
  }
}

Ein Client, der diese Methode asynchron ausführen möchte, kann einen Delegaten deklarieren und diesem die Adresse der Methode AsyncTestProc übergeben:


public delegate void MyDelegate();
...
MyDelegate del = new MyDelegate(obj.AsyncTest);
del.BeginInvoke(...);

Das reicht bereits aus, um AsyncTestProc in einem separaten Thread abzuarbeiten.

Dem Aufruf von BeginInvoke müssen Argumente übergeben werden, die unsere Anweisung noch nicht enthält. Sehen wir uns deshalb nun die Definition von BeginInvoke an.


public IAsyncResult BeginInvoke([Parameterliste ,
                                AsyncCallback, Object); 

Aufgerufen wird BeginInvoke auf die Instanz eines Delegaten, der auf eine bestimmte Methode zeigt. Weist die aufzurufende Methode eine Parameterliste auf, müssen die erforderlichen Argumente von BeginInvoke an die Methode weitergeleitet werden. Dazu dient die optionale Parameterliste.

Theoretisch wäre das bereits vollkommen ausreichend, um die aufgerufene Methode asynchron auszuführen. Meistens benötigt der Aufrufer aber Kenntnis von der Beendigung der asynchronen Ausführung, beispielsweise wenn er die Rückgabewerte verarbeitet. Folglich muss es eine Möglichkeit geben, die es der asynchron aufgerufenen Methode ermöglicht, den Aufrufer davon zu unterrichten, dass sie ihre Operationen beendet hat. Dabei kann es sich nur um den Aufruf einer Methode im Initiator der asynchronen Operation handeln.

Konsequenterweise muss der asynchron aufgerufenen Methode die Adresse der Rückrufmethode im Aufrufer bekannt ist. Das klingt wieder verdächtig nach einem Delegaten – und tatsächlich ist dem so, denn dem Aufruf von BeginInvoke werden nicht nur die Argumente übergeben, welche die asynchron aufgerufene Methode benötigt, sondern darüber hinaus auch ein Objekt vom Typ AsyncCallback, bei dem es sich um den erforderlichen Delegaten handelt.

Die Definition des Delegaten AsyncCallback lautet:


public delegate void AsyncCallback(IAsyncResult ar);

Die Methode, die aus der asynchron ausgeführten zurückgerufen wird, muss den Rückgabetyp void aufweisen und einen Parameter vom Typ IAsyncResult definieren.

BeginInvoke verfügt noch über einen weiteren Parameter vom Typ object. Hier kann beim Start der asynchronen Operation ein beliebiges Objekt übergeben werden, das Informationen beliebiger Art enthält.

Das hört sich komplizierter an, als es tatsächlich ist. Daher wollen wir den Ablauf schrittweise an einem kleinen Beispiel verfolgen. Gegeben seien dazu die beiden Klassen Program und ClassA wie folgt:


class Program {
  static void Main(string[] args) {
    // Anweisungen
  }
}
class ClassA {
  public void AsyncTest() {
    // Anweisungen
  }
}

Aus Main heraus soll die Methode AsyncTest in der Klasse ClassA asynchron aufgerufen werden. Diese Forderung bewirkt, dass wir BeginInvoke auf einen Delegaten aufrufen müssen, der die asynchron auszuführende Methode im Objekt vom Typ ClassA beschreibt. Dazu wird zunächst auf Klassenebene der Klasse Program ein Delegat mit


public delegate void MyDelegate();

deklariert. Anschließend verschaffen wir uns ein Objekt vom Typ des Delegaten, dem als Argument die asynchron auszuführende Methode übergeben wird.


private static MyDelegate del;
...
del = new MyDelegate(obj.AsyncTest);

Mit


del.BeginInvoke(...);

wird die asynchrone Ausführung von AsyncTest in einem Hintergrundthread gestartet. Allerdings ist die Anweisung noch unvollständig – symbolisiert durch die Punkte. Wir sollten in Program nämlich noch eine Methode bereitstellen, über die der Hintergrundthread das Objekt vom Typ Program über das Ende seiner Operation benachrichtigt. Die Definition der Rückrufmethode muss der des Delegaten AsyncCallback entsprechen, demnach also einen Parameter vom Typ IAsyncResult enthalten. Wir nennen diese Methode MyCallBackProc.


class Program {
  ...
  static void Main(string[] args) {...}
  public static void MyCallbackProc(IAsyncResult ar) {
    // Anweisungen
  }
}

Das Objekt vom Typ IAsyncResult entspricht dem Rückgabewert von BeginInvoke. Es veröffentlicht insgesamt sechs Eigenschaften. Dazu gehört unter anderem auch IsCompleted. Über IsCompleted kann der Aufrufer jederzeit feststellen, ob die asynchrone Ausführung bereits beendet ist. Eine zweite, sehr interessante Eigenschaft ist AsyncState, die genau das Objekt abruft, das als letzter Parameter dem Aufruf von BeginInvoke übergeben worden ist. Sie werden später in einem anderen Beispiel die sinnvolle Auswertung dieses Objekts sehen.

Wir wollen nun unser Beispiel komplettieren und sowohl innerhalb des Servers als auch innerhalb des Clients Code einsetzen, der tatsächlich einige Zeit in Anspruch nimmt, damit wir den Effekt des asynchronen Aufrufs tatsächlich beobachten können.


// --------------------------------------------------------------
// Beispiel: ...\Kapitel 11\AsynchronerAufruf_1
// --------------------------------------------------------------
class Program {
  public delegate void MyDelegate();
  private static MyDelegate del;
  static void Main(string[] args) {
    ClassA obj = new ClassA();
    // Delegat, der die asynchron aufzurufende Methode beschreibt
    del = new MyDelegate(obj.AsyncTest);
    // Delegate vom Typ AsyncCallback beschreibt die Methode, die 
    // der Server nach Beendigung der asynchronen Ausführung aufruft
    AsyncCallback callback = new AsyncCallback(MyCallbackProc);
    // die Methode AsyncTest in ClassA asynchron aufrufen
    del.BeginInvoke(callback, null);
    // zeitaufwändige Ausführung
    for(int i = 0; i <= 100; i++) {
      Console.Write(".P.");
      Thread.Sleep(10);
    }
    Console.ReadLine();
  }
  // die zurückgerufene Methode
  public static void MyCallbackProc(IAsyncResult ar) {
    Console.Write("Ich habe fertig.");
  }
}
class ClassA {
  // asynchron aufzurufende Methode
  public void AsyncTest() {
    // zeitintensive Ausführung
    for(int i = 0; i <= 30; i++) {
      Console.Write(".X.");
      Thread.Sleep(10);
    }
  }
}

In der Abbildung 11.11 ist das Ergebnis des Aufrufs zu sehen. Es ist eindeutig zu erkennen, dass .P. bzw. .X. mehr oder weniger abwechselnd ausgegeben werden, denn beide Methoden arbeiten parallel. Beendet wird die asynchrone Operation durch den Rückruf von MyCallbackProc, was durch die Ausgabe des bekannten Satzes »Ich habe fertig« bestätigt wird.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 11.11   Ausgabe eines asynchronen Aufrufs

Beispielprogramm – Pumpen asynchron einschalten

Das Beispielprogramm der Pumpenschaltung aus Kapitel 7 zeigte, wie die Pumpen mittels Rückruf den Client über das Einschalten der Motoren in Kenntnis setzten. In diesem Beispiel wurden die Pumpen der Reihe nach eingeschaltet. Dabei wurde jedoch nicht berücksichtigt, dass der Startvorgang durchaus eine gewisse Zeit in Anspruch nehmen kann. Jetzt soll das Programm so abgeändert werden, dass die Einschaltvorgänge nicht mehr hintereinander, sondern gleichzeitig erfolgen.


// --------------------------------------------------------------
// Beispiel: ...\Kapitel 11\Pumpeneinschalten
// --------------------------------------------------------------
...
public delegate void PumpDelegate();
// ---------------- Client-Klasse -----------------
public class Client {
  static void Main(string[] args) {
    Client client = new Client();
    // Delegat auf die von den Pumpen zurückzurufende Methode
    AsyncCallback callback = new AsyncCallback(client.PumpInfo);
    // Steuerklasse instanziieren
    ControlPumps obj = new ControlPumps(callback);
    // Pumpen bereitstellen
    PumpeA p1 = new PumpeA("Nummer 1");
    obj.AddPump(p1);
    PumpeA p2 = new PumpeA("Nummer 2");
    obj.AddPump(p2);
    PumpeA p3 = new PumpeA("Nummer 3");
    obj.AddPump(p3);
    // Pumpen starten
    obj.StartAllPumps();
    // Simulation von Clientoperationen
    for(int i = 0; i < 10; i++) {
      Console.WriteLine("ich warte...");
      Thread.Sleep(1000);
    }
    Console.ReadLine();
  }
  // zurückzurufende Methode
  public void PumpInfo(IAsyncResult ar) {
    Console.WriteLine("Die Pumpe {0} ist angelaufen.", ar.AsyncState);
  }
}
// ---------------- ControlPumps-Klasse -----------------
public delegate void CallbackDelegate(IAsyncResult ar);
public class ControlPumps {
  private ArrayList colPumps = new ArrayList();
  private AsyncCallback callback;
  // Konstruktor
  public ControlPumps(AsyncCallback callback) {
    this.callback = callback;
  }
  // Pumpen der Collection hinzufügen
  public void AddPump(PumpeA pObj) {
    colPumps.Add(pObj);
  }
  // alle Pumpen der Reihe nach einschalten
  public void StartAllPumps() {
    // die Liste der Pumpen durchlaufen
    foreach(PumpeA pumpe in colPumps) {
      // Delegat auf die Startmethode
      PumpDelegate delPump = new PumpDelegate(pumpe.SwitchOnA);
      // Startmethode asynchron aufrufen
      delPump.BeginInvoke(callback, pumpe.Bezeichner);
    }
  }
}
// ---------------- PumpeA-Klasse -----------------
public class PumpeA {
  private string bezeichner;
  // Konstruktor
  public PumpeA(string bezeichner) {
    this.bezeichner = bezeichner;
  }
  // Eigenschaftsmethode
  public string Bezeichner {
    get{return this.bezeichner;}
  }
  // Methode zum Einschalten
  public void SwitchOnA() {
    Random rnd = new Random();
    int dauer = rnd.Next(100, 5000);
    Thread.Sleep(dauer);
  }
}

In diesem Beispielprogramm werden nicht die Delegaten auf die Startmethoden an die steuernde Klasse übergeben, sondern die Referenzen auf die Pumpen. Damit wir später die Pumpen identifizieren können, die sich bei der Rückrufmethode nach dem erfolgreichen Starten melden, ist die Pumpenklasse um einen Konstruktor und die Eigenschaft Bezeichner ergänzt worden. Die Eigenschaft liefert den Inhalt des Feldes bezeichner zurück.

Weil alle Pumpen dieselbe Methode im Client zurückrufen, bietet es sich an, der Steuerklasse den Delegaten auf diese Methode zu übergeben:


AsyncCallback callback = new AsyncCallback(client.PumpInfo);
ControlPumps obj = new ControlPumps(callback);

Die Referenz auf den übergebenen Delegaten hält das Objekt der Klasse ControlPumps in einer privaten Variablen vor.

Nachdem alle Pumpen in der Steuerklasse registriert sind, können sie durch den Aufruf von StartAllPumps im Client gestartet werden. In einer foreach-Schleife werden alle in der internen Auflistung gespeicherten Pumpenobjekte durchlaufen und für jedes Objekt ein Delegat erzeugt, der auf die objektspezifische Startmethode zeigt. Mit


delPump.BeginInvoke(callback, pumpe.Bezeichner);

wird anschließend BeginInvoke auf diesem Delegaten aufgerufen. Damit gewährleisten wir, dass die Startmethode der jeweiligen Pumpe in einem eigenen Thread ausgeführt wird. Das Besondere an diesem Aufruf ist, dass wir dem zweiten Parameter ein Zustandsobjekt übergeben – es handelt es dabei um die Zeichenfolge, die eine Pumpe eindeutig beschreibt.

In der Rückrufmethode des Clients kann das dem BeginInvoke-Aufruf übergebene Zustandsobjekt, das von der Steuerklasse an den Thread weitergereicht und von der Rückrufmethode übergeben wird, ausgewertet werden. Dazu dient die Eigenschaft AsyncState des IAsyncResult-Parameters. Damit wissen wir nicht nur, dass eine Pumpe angelaufen ist, sondern gleichzeitig auch, um welche es sich handelt.


public void PumpInfo(IAsyncResult ar) {
   Console.WriteLine("Die Pumpe {0} ist angelaufen.", ar.AsyncState);
}


Galileo Computing

11.4.2 Asynchroner Aufruf mit Rückgabewerten  downtop

Möglicherweise liefert die asynchrone Methode als Resultat ihrer Operation einen Rückgabewert. Vielleicht werden auch über die Parameterliste Ergebnisse bereitgestellt. Wird aus dem Hintergrundthread heraus die Rückrufmethode des Initiators der asynchronen Operation aufgerufen, stehen die Ergebnisse jedoch nicht automatisch zur Verfügung, sie müssen ausdrücklich abgerufen werden. Dazu dient die Methode EndInvoke des Delegaten.


public Datentyp EndInvoke([Parameterliste,] IAsyncResult);

Wie bei BeginInvoke müssen Sie auch EndInvoke eine vorgeschriebenen Parameterliste übergeben, die nicht identisch mit der Parameterliste von BeginInvoke ist: Sie darf nur die Referenzparameter der asynchronen Methode enthalten, damit diese ihre Resultate dort hineinschreiben kann. Die Angabe der Werteparameter ist nicht erlaubt. Der einzige grundsätzlich immer zwingend erforderliche Parameter ist vom Typ IAsyncResult. Hier wird das Objekt übergeben, das die Rückrufmethode des Clients vom Server erhalten hat.

Wir wollen nun das Beispiel AsynchronerAufruf_1 ändern, um zu sehen, wie eine asynchrone Methode behandelt wird, die sowohl Werte- als auch Referenzparameter erwartet und darüber hinaus auch noch einen Rückgabewert hat. Dazu implementieren wir die Methode AsyncTest wie folgt:


public string AsyncTest(int x, ref long y) {
  // zeitaufwändige Ausführung
  for(int i = 0; i <= 30; i++) {
    Console.Write(".X.");
    Thread.Sleep(10);
  }
  y = 12345;
  return "Ich habe fertig.";
}

Die Parameterliste enthält jetzt den Referenzparameter y und den Werteparameter x, außerdem liefert die Methode eine Zeichenfolge zurück.

Die Änderung der Signatur hat natürlich auch im auslösenden Thread Konsequenzen. Der Delegat, der den Aufruf der Methode kapselt, muss an die veränderten Bedingungen angepasst werden:


public delegate string MyDelegate(int x, ref long y);

Gleiches gilt auch für den Start der asynchronen Bearbeitung, denn nun reicht es nicht mehr aus, mit BeginInvoke einfach nur einen Delegaten auf die Rückrufmethode zu übergeben sowie die Referenz auf ein Objekt, in das der asynchrone Aufruf Informationen schreiben könnte. Wir müssen stattdessen auch die Parameter der asynchronen Methode in der richtigen Reihenfolge bedienen:


del.BeginInvoke(intVar, ref lngVar, callback, null);

AsyncTest nimmt nun eine Kopie des int-Wertes und die Adresse des long-Wertes entgegen, kann mit diesen die erforderlichen Operationen ausführen und zum Abschluss durch Aufruf der über callback bekannt gegebenen Adresse die Methode MyCallbackProc informieren.

Der Implementierung der Rückrufmethode kommt nun eine entscheidende Bedeutung zu. Es gilt, sowohl den Rückgabewert als auch den in diesem Fall geänderten Inhalt der Variablen lngVar auszuwerten. Dem Aufruf von EndInvoke übergeben wir die Adresse von lngVar und holen uns den Rückgabewert an der Konsole ab:


public static void MyCallbackProc(IAsyncResult ar) {
  // zeigt den Rückgabewert der asynchronen Methode an
  Console.Write(del.EndInvoke(ref lngVar, ar));
  // schreibt den Inhalt des Referenzparameters lngVar
  Console.Write("..Wert y = {0}", lngVar);
}

Die Konsolenausgabe bestätigt, dass unser Unterfangen von Erfolg beschieden ist: Wir erhalten sowohl die Zeichenfolge als auch den veränderten Inhalt des Feldes intVar.

Zum Abschluss fassen wir das Beispielprogramm noch einmal zusammen.


// --------------------------------------------------------------
// Beispiel: ...\ Kapitel 11\AsynchronerAufruf_2
// --------------------------------------------------------------
class Program {
  // --------- G e ä n d e r t --------------
  public delegate string MyDelegate(int x, ref long y);
  // -----------E r g ä n z t ---------------
  private static MyDelegate del;
  private static int intVar = 4711;
  private static long lngVar;
  static void Main(string[] args) {
    ClassA obj = new ClassA();
    // Delegat, der die asynchron aufzurufende Methode beschreibt
    del = new MyDelegate(obj.AsyncTest);
    // Delegate vom Typ AsyncCallback beschreibt die Methode, die nach
    // Beendigung der asynchronen Ausführung aufgerufen wird
    AsyncCallback callback = new AsyncCallback(MyCallbackProc);
    // die Methode AsyncTest in ClassA asynchron aufrufen
    // --------- G e ä n d e r t --------------
    del.BeginInvoke(intVar, ref lngVar, callback,null);
    // zeitaufwändige Ausführung
    for(int i = 0; i <= 100; i++) {
      Console.Write(".P.");
      Thread.Sleep(10);
    }
    Console.ReadLine();
  }
  public static void MyCallbackProc(IAsyncResult ar) {
    // --------- G e ä n d e r t --------------
    // zeigt den Rückgabewert der asynchronen Methode an
    Console.Write(del.EndInvoke(ref lngVar, ar));
    // schreibt den Inhalt des Referenzparameters lngVar
    Console.Write("..Wert y = {0}", lngVar);
  }
}
class ClassA {
  // --------- G e ä n d e r t --------------
  public string AsyncTest(int x, ref long y) {
    // zeitaufwändige Ausführung
    for(int i = 0; i <= 30; i++) {
      Console.Write(".X.");
      Thread.Sleep(10);
    }
    y = 12345;
    return "Ich habe fertig.";
  }
}

Interessant ist, innerhalb der Rückrufmethode ein wenig zu spielen. Wenn Sie beispielsweise der Meinung sind, Sie könnten auch mit


// funktioniert nicht!!!
public static void MyCallbackProc(IAsyncResult ar) {
  Console.Write(ref lngVar);
}

den Inhalt der als Referenz übergebenen Variablen lngVar auswerten, werden Sie Schiffbruch erleiden. Zu keinem Zeitpunkt schreibt die Methode des Hintergrundthreads den Inhalt 12345 in das Original. Ohne den Aufruf von EndInvoke bleibt das Original unbeeinflusst – es fehlt der äußere Anstoß, die neuen Daten zu übernehmen. Erst mit EndInvoke stehen diese zur Verfügung.

Wir könnten auch auf die Idee kommen, eine neue Variable vom Typ des Interface IAsyncResult zu deklarieren und diese an EndInvoke weiterreichen anstelle der im Parameter der Rückrufmethode entgegengenommenen:


// funktioniert auch nicht!!!
public void MyCallbackProc(IAsyncResult ar) {   
  IAsyncResult newAsyncResult;
  ...
  Console.Write(del.EndInvoke(ref lngVar, newAsyncResult));
  Console.Write(lngVar);
}

Dieser Aufruf wird wirkungslos bleiben und zur Laufzeit im Nirwana enden. Denn was die Rückrufmethode MyCallbackProc im Parameter ar entgegennimmt, dient ihr gleichzeitig zum Aufruf eines ganz bestimmten Objekts des Servers – der IASyncResult-Parameter schafft die Möglichkeit der wechselseitigen Kommunikation. Wir haben es hier gewissermaßen mit einer Hintereinanderschaltung von Rückrufen zu tun.


Galileo Computing

11.4.3 Eine Klasse mit asynchronen Methodenaufrufen  toptop

Am Anfang dieses Abschnitts wurde schon darauf hingewiesen, dass einige Klassen der .NET-Klassenbibliothek Methoden mit asynchroner Verarbeitung anbieten. Die Klasse FileStream im Namespace System.IO ist ein Beispiel dafür. Es werden allerdings nicht die Methoden BeginInvoke und EndInvoke aufgerufen, sondern zwei ähnlich lautende: BeginRead und EndRead bzw. BeginWrite und EndWrite.

Wir wollen uns nun ansehen, wie eine Klasse aufgebaut ist, die ähnlich wie FileStream implementiert ist. Dabei lernen wir einerseits, wie wir die asynchronen Methoden der Klassen des .NET Frameworks behandeln müssen, andererseits aber auch, diese Technik in eigenen Klassen zu nutzen.

Am Anfang steht die Idee, eine Methode zu entwickeln, von der wir annehmen, dass sie in Abhängigkeit von den Umgebungsbedingungen und der Art der Operation eine längere Zeit zur Bearbeitung in Anspruch nehmen kann. Wir wollen diese Methode nachfolgend Calculate nennen, die Klasse dazu Server.


class Server {
  public int Calculate(int x) {
    Console.Write("---Bearbeitung startet---"); 
    for (int i = 0; i <= 20; i++) {
      Console.Write(".X.");
      Thread.Sleep(10);
    }
    Console.Write("---Bearbeitung beendet---");
    return x * x;
 }
}

Die for-Schleife simuliert eine länger andauernde Operation. Diese Implementierung arbeitet synchron. Da wir uns bewusst sind, dass Calculate vielleicht auch eine Stunde zur vollständigen Ausführung brauchen könnte (wir sind mit unserer Annahme sehr großzügig), bieten wir zusätzlich eine asynchrone Variante an. Dazu benötigen wir zwei weitere Methoden, die einer allgemeinen Konvention folgend als BeginXxx und EndXxx bezeichnet werden – in unserer Klasse demnach BeginCalculate und EndCalculate. Die noch unvollständige Klassenstruktur sieht dann folgendermaßen aus:


class Server {
  // Methode Calculate wird synchron ausgeführt
  public int Calculate(int x) {
    // Anweisungen
  }
  // Start der asynchronen Ausführung
  public ... BeginCalculate(...) {
    // Anweisungen, u. a. der Aufruf von Calculate
  }
  // Beenden der asynchronen Ausführung
  public ... EndCalculate(.) {
    // Anweisungen
  }
}

An dieser Stelle kommt es zu der wichtigsten Entscheidung überhaupt. Was wir beabsichtigen, ist die asynchrone Ausführung der Methode Calculate. Asynchronität heißt aber auch, dass ein weiterer Thread gestartet werden muss, sobald die Methode BeginCalculate aufgerufen wird. Wenn wir in dieser Methode ein Objekt vom Typ Thread erzeugen und seinem Konstruktor einen Delegaten übergeben, bräuchten wir auch noch ein Objekt, welches das Interface IAsyncResult implementiert, müssten zwangsläufig dessen Methoden implementieren usw.

Die Entwicklung auf diese Weise zu gestalten, ist sehr aufwändig. Es gibt eine viel einfachere Lösung, da die beiden Methoden BeginInvoke und EndInvoke genau das leisten, was wir brauchen. Also benutzen wir sie auch, um das Ziel effizient zu erreichen. Dazu wird die Logik, die in den Abschnitten 11.4.2 und 11.4.3 beschrieben wurde, innerhalb der Klasse Server implementiert.


// --------------------------------------------------------------
// Beispiel: ...\ Kapitel 11\AsynchronerAufruf_3
// --------------------------------------------------------------
class Server {
  // Deklaration eines Delegaten, der den Funktionsaufruf
  // von 'Caluculate' beschreibt
  public delegate int CalculateHandler(int x);
  CalculateHandler del;
  // Methode Calculate wird synchron ausgeführt
  public int Calculate(int x) {
    Console.Write("---Bearbeitung startet---"); 
    for (int i = 0; i <= 20; i++) {
      Console.Write(".X.");
      Thread.Sleep(10);
    }
    Console.Write("---Bearbeitung beendet---");
    return x * x;
  }
  // Start der asynchronen Ausführung
  public IAsyncResult BeginCalculate(int intVar,
            AsyncCallback asyncCallback, object state) {
    del = new CalculateHandler(Calculate);
    // Aufruf der Methode Calculate, die in einem eigenen 
    // Thread ausgeführt wird
    return del.BeginInvoke(intVar, asyncCallback, null);
  }
  // Beenden der asynchronen Ausführung
  public int EndCalculate(IAsyncResult asyncResult) {
    return del.EndInvoke(asyncResult);
  }
}

Dem Aufruf der Methode BeginCalculate werden die Daten übergeben, welche die Methode Calculate für ihre Operation benötigt. In unserem Beispiel handelt es sich nur um einen als Werteparameter deklarierten Integer. Der zweite Parameter erhält die Referenz auf einen Delegaten, der die Rückrufmethode im Aufrufer beschreibt. Der dritte und letzte Parameter dient dazu, ein Objekt bereitzustellen, mit dem Daten zwischen dem aufrufenden und dem aufgerufenen Objekt ausgetauscht werden. Ein solches Objekt ist in unserem Beispielcode nicht vorgesehen.

Der Aufruf von BeginCalculate orientiert sich an dem von BeginInvoke – und das ist typisch für Klassen im .NET Framework, die asynchrone Methoden offen legen. Unter ähnlicher Prämisse wird auch EndCalculate implementiert, der Rückgabewert des internen EndInvoke-Aufrufs wird zum Rückgabewert der Instanzmethode.

Es bleibt zum Schluss noch zu testen, ob die Klassenimplementierung auch unseren Anforderungen genügt. Dazu entwickeln wir einen Client mit der Methode Start zum Aufruf der asynchronen Ausführung und einer Methode Results, die als Rückruffunktion vom Server angesteuert wird.


class Client {
  private Server myObj = new Server();
  static void Main(string[] args) {
    Client myClient = new Client();
    myClient.Start();
  }
  public void Start() {
    int iVar = 23;
    AsyncCallback callback = new AsyncCallback(this.Results);
    // Aufruf der asynchronen Ausführung
    IAsyncResult ia = myObj.BeginCalculate(iVar, callback, null);
    for (int i = 0; i <= 100; i++) {
      Console.Write(".{0}.", i);
      Thread.Sleep(5);
    }
    Console.ReadLine();
  }
  // diese Methode wird vom Server aufgerufen
  public void Results(IAsyncResult asyncResult) {
    // das Ergebnis der asynchronen Operation abholen
    int res = myObj.EndCalculate(asyncResult);
    Console.Write("---Resultat = {0} ", res);
    Console.Write("---FERTIG---");
  }
}

Die Ausgabe an der Konsole wird wie in der folgenden Abbildung gezeigt aussehen.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 11.12   Ausgabe des Beispiels »AsynchronerAufruf_3«


Hinweis   Sie sollten grundsätzlich immer, wenn die Methode einer Klasse eine länger andauernde Operation ausführt, neben der asynchronen Variante auch die synchron arbeitende Methode anbieten, um dem Benutzer die Entscheidung zu überlassen, ob er die synchrone oder asynchrone Variante aufrufen möchte.

 << zurück
  
  Zum Katalog
Zum Katalog: Visual C# 2005
Visual C# 2005
bestellen
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Fortgeschrittene Programmierung mit Visual C# 2005






 Fortgeschrittene
 Programmierung
 mit Visual C# 2005


Zum Katalog: Einstieg in Visual C# 2005






 Einstieg in
 Visual C# 2005


Zum Katalog: Einstieg in Visual Basic 2005






 Einstieg in
 Visual Basic 2005


Zum Katalog: Visual Basic 2005






 Visual Basic 2005


Zum Katalog: Java ist auch eine Insel






 Java ist auch eine
 Insel


Zum Katalog: Konzepte und Lösungen für Microsoft-Netzwerke






 Konzepte und
 Lösungen für
 Microsoft-Netzwerke


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo








Copyright © Galileo Press 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de